查看原文
其他

在 Android 上做一个 1:1 高达

fundroid AndroidPub 2022-07-13
最近看到一则新闻,一个 1:1 的自由高达落户上海金桥。

作为高达爱好者,我很想去现场感受一下这种真实的压迫感,可惜最近没机会去上海。 不过这难不倒我,借助 AR 技术我自己动手做了一个 1:1 的高达



怎么样,在手机里也能获得身临其境的体验~

什么是 AR (Augemented Reality)

AR(增强现实)是近几年新兴的技术,他可以将3D模型等虚拟元素模拟仿真后应用到现实世界中,虚拟与现实,两种信息互为补充,从而实现对真实世界的“增强”。

不少人会将 AR(增强现实) 与 VR(虚拟现实)相混淆,两者的区别在于虚拟元素的占比:

  • VR:看到的场景和人物全是假的,是把你的意识代入一个虚拟的世界。
  • AR:看到的场景和人物一部分是真一部分是假,是把虚拟的信息带入到现实世界中。

VR 技术的研究已经有30多年的历史了,而 AR 则年轻得多,随着智能手机以及智能穿戴设备的普及而逐渐被人们熟知。相对于 VR,AR 的开发门槛低得多,只要有一台智能手机,借助 Google 提供的 ARCore,人人都可以开发出自己的 AR 应用。

Google ARCore

ARCore  是 Google 提供的 AR 解决方案,为开发者提供 API,可以通过 Android , iOS 等手机平台感知周边环境,打造沉浸式的 AR 体验。

ARCore 为开发者提供了三大能力:

1. 动作追踪:通过识别相机图像中的可视特征点来跟踪拍摄者的位置,从而决定虚拟元素的相对位置变化

2. 环境理解:识别常见水平或垂直表面(如表格或墙壁)上的特征点群集,还可以确定平面边界,将虚拟对象放置在平面上

3. 光线预测:预测当前场景的光照条件,可以使用此照明信息来照亮虚拟 AR 对象,模拟出物体在现实世界中的影子

Sceneform SDK

ARCore 为 AR 提供了周边环境的感知能力,但一个完整的 AR 应用还需要处理 3D 模型的渲染,这要借助 OpenGL ES 来完成,学习成本很高。官方意识到这个问题,在 2017 年推出 ARCore 之后,紧跟着 2018 年的 IO 大会上推出了 Sceneform  这个在 Android 上的 3D 图像渲染库。

.obj , .fbx 或  .gltf 等常见的 3D 模型文件格式,虽然可以在主流的 3D 软件中通用,但在 Android 中,我们只能通过 OpenGL 代码对其进行渲染。而 Sceneform 可以将这些格式的模型文件,连同所依赖的资源文件(.mtl, .bin, .png 等) 转换为 .sfa 和 .sfb 格式。后者是可供 Sceneform 渲染的二进制模型文件, 前者是具有可读性的摘要文件,用来描述后者。

相比于 OpenGL , Sceneform 的使用要简单得多,而且 sfb 还可以通过 Sceneform 提供的 AS 插件在 IDE 中进行模型预览。

接下来,通过 Sceneform 和 ARCore 来实现我的 1:1 高达


1. Gradle 添加依赖

新建 AndroidStudio 工程,在 root 的 build.gradle 中添加 Sceneform 插件

dependencies { 
    classpath 'com.google.ar.sceneform:plugin:1.17.1' 
}

接着在 app 的 build.gradle 中依赖 ARCore 和 Sceneform 的 aar

dependencies {
    ...
    implementation 'com.google.ar:core:1.26.0'
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1'
    implementation 'com.google.ar.sceneform:core:1.17.1'
}

2. Manifest 申请权限


<uses-permission android:name="android.permission.CAMERA"/>

<!-- 此 App 在 GooglePlay 中只会对支持 ARCore 的设备可见 -->
<uses-feature android:name="android.hardware.camera.ar" android:required="true"/>

<application …>
    …

  <!-- 当安装 App 时,如果设备没有安装 ARCore,GooglePlay 会自动为其安装 -->
  <meta-data android:name="com.google.ar.core" android:value="required" />

</application>

3. 布局文件

ARFragment 可以用来承载 AR 场景、响应用户行为,Android 中显示虚拟元素的最简单的方法就是在布局中添加一个 ARFRagment :

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

  
<fragment
        android:id="@+id/ux_fragment"
        android:name="com.google.ar.sceneform.ux.ArFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="9" />


</FrameLayout>

4. 制作 sfb 模型文件

3D 模型一般是通过 Maya 或 3D Max 等专业软件制作的,3D 建模爱好者会将自己的作品上传到一些设计网站供大家免费或有偿下载。

模型网站:https://sketchfab.com/ 

我们可以在网站上下载常见格式的 3D 模型文件。以 .obj 为例,obj 文件中描述了多边形的顶点和片段信息等, 此外还有颜色、材质等信息存储在配套的 .mtl 文件中 , 我们将下载的 obj/mtl/png 等模型文件拷贝到非 assets 目录下,这样可以避免打入 apk。

例如 app/sampledata

我们在 build.gtadle 通过 sceneform.asset(...) 添加 obj > sfb 的配置如下

sceneform.asset('sampledata/msz-006_zeta_gundam/scene.obj',
        'default',
        'sampledata/msz-006_zeta_gundam/scene.sfa',
        'src/main/assets/scene')

sampledata/msz-006_zeta_gundam/scene.obj 是 obj 源文件位置, src/main/assets/scene 是生成的 sfb 目标路径,我们将目标文件生成在 assets/ 中,打入 apk ,便于在运行时加载。

gradle 配置完后,sync 并 build 工程,build 过程中,会在 assets/ 中生成同名 sfb 文件

5. 加载 sfb 模型

//MainActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        arFragment = supportFragmentManager.findFragmentById(R.id.ux_fragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitResult, plane, motionEvent ->
        
            if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING ||
                    state != AnchorState.NONE) {
                return@setOnTapArPlaneListener
            }

            val anchor = hitResult.createAnchor()
            placeObject(ux_fragment, anchor, Uri.parse("scene.sfb"))

        }
        
    }
        

ARFragment 能够响应在 AR 场景中的用户点击行为,在点击的位置中添加虚拟元素, Uri.parse("scene.sfb") 用来获取 assets 中生成的模型文件。

    private fun placeObject(fragment: ArFragment, anchor: Anchor, model: Uri) {
        ModelRenderable.builder()
                .setSource(fragment.context, model)
                .build()
                .thenAccept {
                    addNodeToScene(fragment, anchor, it)
                }
                .exceptionally { throwable : Throwable ->
                   Toast.makeText(arFragment.getContext(), "Error:$throwable.message", Toast.LENGTH_LONG).show();
                   return@exceptionally null
                }
    }
    

Sceneform 提供 ModelRenderable 用于模型渲染。通过 setSource 加载 sfb 模型文件

   private fun addNodeToScene(fragment: ArFragment, anchor: Anchor, renderable: Renderable) {
        val anchorNode = AnchorNode(anchor)
        val node = TransformableNode(fragment.transformationSystem)
        node.renderable = renderable
        node.setParent(anchorNode)
        fragment.arSceneView.scene.addChild(anchorNode)
        node.select()
    }

ARSceneView 持有一个 Scene, Scene 是一个树形数据结构,作为 AR 场景的根节点,各种虚拟元素将作为其子节点被添加到场景中进行渲染

val node = TransformableNode(fragment.transformationSystem)
node.renderable = renderable
node.setParent(anchorNode)

所以,渲染 3D 模型,其实就是添加一个 Node 并为其设置 Renderable 的过程。

hitResult 是用户点击的位置信息,Anchor基于 hitResult 创建锚点,这个锚点作为子节点被添加到 Scene 根节点中,同时又作为 TransformableNode 的父节点。TransformableNode 用来承载 3D 模型, 它可以接受手势进行拖拽或者放大缩小, 添加到 Archor 就相当于把 3D 模型放置到点击的位置上。

6. 完整代码

class MainActivity : AppCompatActivity() {
    lateinit var arFragment: ArFragment
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (!checkIsSupportedDeviceOrFinish(this)) return
        setContentView(R.layout.activity_main)
        
        arFragment = supportFragmentManager.findFragmentById(R.id.ux_fragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitresult: HitResult, plane: Plane, motionevent: MotionEvent? ->
            if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING)
                return@setOnTapArPlaneListener
            val anchor = hitresult.createAnchor()
            placeObject(arFragment, anchor, R.raw.cube)
        }
    }

    private fun placeObject(arFragment: ArFragment, anchor: Anchor, uri: Int) {
        ModelRenderable.builder()
                .setSource(arFragment.context, uri)
                .build()
                .thenAccept { modelRenderable: ModelRenderable -> addNodeToScene(arFragment, anchor, modelRenderable) }
                .exceptionally { throwable: Throwable ->
                   Toast.makeText(arFragment.getContext(), "Error:$throwable.message", Toast.LENGTH_LONG).show();
                    return@exceptionally null
                }
    }

    private fun addNodeToScene(arFragment: ArFragment, anchor: Anchor, renderable: Renderable) {
        val anchorNode = AnchorNode(anchor)
        val node = TransformableNode(arFragment.transformationSystem)
        node.renderable = renderable
        node.setParent(anchorNode)
        arFragment.arSceneView.scene.addChild(anchorNode)
        node.select()
    }

    private fun checkIsSupportedDeviceOrFinish(activity: Activity)Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            Toast.makeText(activity, "Sceneform requires Android N or later", Toast.LENGTH_LONG).show()
            activity.finish()
            return false
        }
        val openGlVersionString = (activity.getSystemService<Any>(Context.ACTIVITY_SERVICE) as ActivityManager)
                .deviceConfigurationInfo
                .glEsVersion
        if (openGlVersionString.toDouble() < MIN_OPENGL_VERSION) {
            Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
                    .show()
            activity.finish()
            return false
        }
        return true
    }

    companion object {
        private const val MIN_OPENGL_VERSION = 3.0
    }
}

checkIsSupportedDeviceOrFinish 用来检测可运行环境,通过代码也能看到 Sceneform 需要运行在 AndroidN 以及 OpenGL 3.0 以上。

以上就是全部代码了,代码很少,但效果很哇塞

最后

Sceneform 配合 ARCore 可以快速搭建 AR 应用,除了加载静态的 3D 模型以外,Sceneform 也可以加载带动画的模型。随着 “元宇宙” 概念的兴起,Google,Facebook 等巨头必将加大在 AR , VR 等技术上的投入,虚拟现实或将替代移动互联网成为人们新的社交、娱乐场景。

今天就写到这里吧, 我要去跟 AR 小姐姐玩耍去了


- END -


推荐阅读


加好友进交流群,技术干货聊不停


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存